package com.colors.diners.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.StrUtil;
import com.colors.commons.constant.ApiConstant;
import com.colors.commons.constant.PointTypesConstant;
import com.colors.commons.exception.ParameterException;
import com.colors.commons.model.domain.ResultInfo;
import com.colors.commons.model.vo.SignInDinerInfo;
import com.colors.commons.utils.AssertUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.*;

/**
 * @author colors
 * @date 2021/5/14 16:59
 * 签到业务逻辑层
 */
@Service
public class SignService {

    @Resource
    private RestTemplate restTemplate;
    @Resource
    private RedisTemplate redisTemplate;

    @Value("${service.name.server-oauth2}")
    private String oauth2ServerName;

    @Value("${service.name.server-points}")
    private String pointsServerName;

    public Map<String, Boolean> getSignInfo(String accessToken, String dateStr) {
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        Date date = getDate(dateStr);
        String key = buildKey(dinerInfo.getId(), date);
        Map<String, Boolean> signInfo = new TreeMap<>();
        int dayOfMonth = DateUtil.lengthOfMonth(DateUtil.month(date) + 1, DateUtil.isLeapYear(DateUtil.dayOfYear(date)));
        // bitfield user:sign:5:202105 u14 0 ，u 表示无符号 ，14 表示今天是14号，0 表示索引，即从第一天开始
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);

        List<Long> list = redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return signInfo;
        }
        long count = list.get(0) == null ? 0 : list.get(0);

        // 移位操作：先右移再左移，结果未变则表示未签到，结果变了则表示签到了
        for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次数
            LocalDateTime localDateTime = LocalDateTimeUtil.of(date).withDayOfMonth(i);
            boolean flag = count >> 1 << 1 != count;
            signInfo.put(DateUtil.format(localDateTime, "yyyy-MM-dd"), flag);
            count >>= 1;
        }
        return signInfo;
    }

    /**
     * 获取用户签到天数
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public long getSignCount(String accessToken, String dateStr) {
        // 获取用户信息
        SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);
        // 构建key
        String key = buildKey(dinerInfo.getId(), date);
        // bitcount
        return (Long) redisTemplate.execute((RedisCallback<Long>) con -> con.bitCount(key.getBytes())
        );
    }


    /**
     * 签到以及连续签到统计
     *
     * @param accessToken
     * @param dateStr
     * @return
     */
    public int doSign(String accessToken, String dateStr) {
        // 获取用户信息
        SignInDinerInfo diner = loadSignInDinerInfo(accessToken);
        // 获取日期
        Date date = getDate(dateStr);
        // 获取日期对应的天数，即多少号
        int offset = DateUtil.dayOfMonth(date) - 1;
        // 构建 key user:sign:id:yyyyMM
        String signKey = buildKey(diner.getId(), date);
        // 查看是否签到
        Boolean isSign = redisTemplate.opsForValue().getBit(signKey, offset);
        AssertUtil.isTrue(isSign, "当前日期已签到");
        // 签到
        redisTemplate.opsForValue().setBit(signKey, offset, true);
        // 统计连续签到的次数
        int count = getContinuousSignCount(diner.getId(), date);
        return addPoints(count,diner.getId());
    }

    /**
     * 统计连续签到的次数
     *
     * @param dinerId
     * @param date
     * @return
     */
    private int getContinuousSignCount(Integer dinerId, Date date) {
        // 当前日期是几号
        int dayOfMonth = DateUtil.dayOfMonth(date);
        // 构建 key
        String key = buildKey(dinerId, date);
        // bitfield user:sign:5:202105 u14 0 ，u 表示无符号 ，14 表示今天是14号，0 表示索引，即从第一天开始
        BitFieldSubCommands bitFieldSubCommands = BitFieldSubCommands
                .create()
                .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth))
                .valueAt(0);

        List<Long> list = redisTemplate.opsForValue().bitField(key, bitFieldSubCommands);
        if (list == null || list.isEmpty()) {
            return 0;
        }
        int signCount = 0;
        long count = list.get(0) == null ? 0 : list.get(0);

        // 移位操作：先右移再左移，结果未变则表示未签到，结果变了则表示签到了
        for (int i = dayOfMonth; i > 0; i--) { // i 表示位移的次数
            if (count >> 1 << 1 == count) {
                // 如果低位是0 且低位所在不是当天，说明连续签到中断
                if (i != dayOfMonth) break;
            } else {
                signCount++;
            }
            // 把最后一位丢弃
            count >>= 1;
        }

        return signCount;
    }

    /**
     * 构建 key user:sign:id:yyyyMM
     *
     * @param dinerId
     * @param date
     * @return
     */
    private String buildKey(Integer dinerId, Date date) {
        return String.format("user:sign:%d:%s", dinerId,
                DateUtil.format(date, "yyyyMM"));
    }

    /**
     * 根据传进来的dateStr获取date
     *
     * @param dateStr
     * @return
     */
    private Date getDate(String dateStr) {
        if (StrUtil.isBlank(dateStr)) {
            return new Date();
        }
        try {
            return DateUtil.parseDate(dateStr);

        } catch (Exception e) {
            throw new ParameterException("请输入yyyy-MM-dd的日期格式");
        }
    }

    /**
     * 获取当前用户信息
     *
     * @param accessToken token
     * @return
     */
    private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
        // 必须需要登录
        AssertUtil.mustLogin(accessToken);

        String url = oauth2ServerName + "user/me?access_token={accessToken}";

        ResultInfo resultInfo = restTemplate.getForObject(url, ResultInfo.class, accessToken);
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            throw new ParameterException(resultInfo.getMessage());
        }

        return BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                new SignInDinerInfo(), false);

    }

    /**
     * 添加用户积分
     *
     * @param count         连续签到次数
     * @param signInDinerId 登录用户id
     * @return 获取的积分
     */
    private int addPoints(int count, Integer signInDinerId) {
        // 签到1天送10积分，连续签到2天送20积分，3天送30积分，4天以上均送50积分
        int points = 10;
        if (count == 2) {
            points = 20;
        } else if (count == 3) {
            points = 30;
        } else if (count >= 4) {
            points = 50;
        }
        // 调用积分接口添加积分
        // 构建请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
        // 构建请求体（请求参数）
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("dinerId", signInDinerId);
        body.add("points", points);
        body.add("types", PointTypesConstant.sign.getType());
        HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
        // 发送请求
        ResponseEntity<ResultInfo> result = restTemplate.postForEntity(pointsServerName,
                entity, ResultInfo.class);
        AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败！");
        ResultInfo resultInfo = result.getBody();
        if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
            // 失败了, 事物要进行回滚
            throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
        }
        return points;
    }
}
